<?php
/**
 * Copyright 2012  SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */
?>
<?php
/**
 * Data object
 * 
 * Contains shared code for all different types of data objects in ESCAPE.
 */
abstract class escape_repository_DataObject
{
	/**
	 * Repository instance.
	 * @var escape_repository_Repository
	 */
	protected $repository;

	/**
	 * Fedora instance.
	 * @var fedora_Fedora
	 */
	protected $fedora;

	/**
	 * Triple store instance.
	 * @var triplestore_TripleStore
	 */
	protected $tripleStore;
	
	protected $uri;
	protected $state;
	protected $properties = null;
	protected $rdfTypes = null;
	protected $fedoraModels = null;
	protected $titleTriple = null;
	protected $createdDate = null;
	protected $lastModifiedDate = null;
	protected $attachmentMimeType = null;
	protected $attachmentFileName = null;
	
	/**
	 * Constructs a data object.
	 * @param escape_repository_Repository $repository Link to the repository object
	 * @param string $uri The URI for the data object
	 * @param simplerdf_RdfResource $rdfResource Prefetched RDF data for object creation
	 * @param boolean $noIndex If true, do not use the index to fetch the object data
	 * @param boolean $allowDeleted If true, also return deleted objects
	 * @return escape_repository_DataObject The data object
	 */
	public function __construct(&$repository, $uri, $rdfResource = null, $noIndex = false, $allowDeleted = false)
	{
		$this->repository =& $repository;
		$this->fedora =& $repository->getFedora();
		$this->tripleStore =& $repository->getTripleStore();
		$this->uri = $uri;

		$pid = $this->fedora->convertUriToPid($uri);
		
		if($rdfResource === null)
		{
			if($noIndex)
			{
				$rdfResource = $this->getDataObjectFromFedora(true);
				if($rdfResource === null)
				{
					throw new escape_repository_exception_NotFound('Object "' . $uri . '" does not exist');
				}
				
				if(!$allowDeleted && $rdfResource->getFirstObject("info:fedora/fedora-system:def/model#state") === 'info:fedora/fedora-system:def/model#Deleted')
				{
					throw new escape_repository_exception_Deleted('Object "' . $uri . '" has been deleted');
				}
			}
			else
			{
				$rdfDocument = $this->tripleStore->fetchStatements($uri, null, null, null, null);
				
				if($rdfDocument->getResourceCount() === 1)
				{
					$rdfResource = $rdfDocument->getResource(0);
					
					if(!$allowDeleted && $rdfResource->getFirstObject("info:fedora/fedora-system:def/model#state") === 'info:fedora/fedora-system:def/model#Deleted')
					{
						throw new escape_repository_exception_Deleted('Object "' . $uri . '" has been deleted');
					}
				}
				else
				{
					throw new escape_repository_exception_NotFound('Object "' . $uri . '" does not exist');
				}
			}
		}

		$this->rdfTypes = $rdfResource->getRdfTypes();
		$this->createdDate = $rdfResource->getFirstObject('info:fedora/fedora-system:def/model#createdDate');
		$this->lastModifiedDate = $rdfResource->getFirstObject('info:fedora/fedora-system:def/view#lastModifiedDate');
		$this->fedoraModels = $rdfResource->getObjects('info:fedora/fedora-system:def/model#hasModel');
		$this->attachmentMimeType = $rdfResource->getFirstObject('info:fedora/fedora-system:def/view#mimeType');
		$this->attachmentFileName = $rdfResource->getFirstObject('http://purl.utwente.nl/ns/escape-system.owl#attachmentFileName');
		$stateUri = $rdfResource->getFirstObject("info:fedora/fedora-system:def/model#state");
		preg_match('~^info:fedora/fedora-system:def/model#(.)~', $stateUri, $matches);
		if(isset($matches[1]))
		{
			$this->state = $matches[1];
		}
		else
		{
			$this->state = $this->fedora->getObjectState($pid);
		}
		
		$this->properties = simplerdf_RdfResource::createRdfResource($rdfResource->getSubject());
		$predicates = $rdfResource->getPredicates();
		foreach($predicates as $predicate)
		{
			if(!preg_match('~^info:fedora/fedora-system:def/~', $predicate))
			{
				$this->properties->addStatement($predicate, $rdfResource->getObjects($predicate));
			}
		}
	}
	
	/**
	 * Update the index for this object (based on the properties of this object instance).
	 */
	public function updateIndex()
	{
		if($this->state === 'A' || $this->state === 'D')
		{
			// object is active or deleted, add it to the index
			$indexRdfResource = clone $this->getProperties();

			$indexRdfResource->addStatement('info:fedora/fedora-system:def/model#createdDate', $this->createdDate);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/view#lastModifiedDate', $this->lastModifiedDate);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/model#hasModel', $this->fedoraModels);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/view#mimeType', $this->attachmentMimeType);
			$indexRdfResource->addStatement('http://purl.utwente.nl/ns/escape-system.owl#attachmentFileName', $this->attachmentFileName);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/model#state', $this->getStateUri());
			
			$this->tripleStore->replaceGraph($this->getUri(), $indexRdfResource);
		}
		else
		{
			// object is pruged, remove it from the index
			$this->tripleStore->deleteGraph($this->getUri());
		}
	}

	/**
	 * Get the raw data for this object without using the index.
	 * @param boolean $allowNonActive If true, non-active objects are also returned (a null is returned otherwise)
	 * @return simplerdf_RdfResource RDF resource
	 */
	protected function getDataObjectFromFedora($allowNonActive = false)
	{
		$pid = $this->getPid();
		$uri = $this->getUri();
		
		$objectState = $this->fedora->getObjectState($pid);
		
		// check if the object is active / exists
		if($allowNonActive || $objectState === 'A')
		{
			// fetch all data required to build the index
			
			// fetch the fedora object profile
			$objectProfile = $this->fedora->getObjectProfile($pid);
			$createdDate = $objectProfile->objCreateDate;
			$lastModifiedDate = $objectProfile->objLastModDate;
			
			// fetch the RELS-EXT datastream
			$relsExtRdfXml = $this->fedora->getDatastreamDissemination($pid, 'RELS-EXT')->stream;
			$relsExtRdfResource = new simplerdf_RdfResource($relsExtRdfXml);
			
			// fetch the SEMANTICS datastream
			$semanticsRdfResource = simplerdf_RdfResource::createRdfResource($uri);
			$semanticsRdfXml = $this->fedora->getDatastreamDissemination($pid, 'SEMANTICS')->stream;
			try
			{
				$semanticsRdfResource = new simplerdf_RdfResource($semanticsRdfXml);
			}
			catch(Exception $exception)
			{
				// no semantic statements available, ignore
			}
			
			// fetch the ATTACHMENT datastream
			$attachmentMimeType = null;
			try
			{
				$attachmentDatastream = $this->fedora->getDatastream($pid, 'ATTACHMENT');
				// check if the attachment datastream is active
				if($attachmentDatastream->state == 'A')
				{
					$attachmentMimeType = $attachmentDatastream->MIMEType;
				}
			}
			catch(Exception $exception)
			{
				// no binary attachment available, ignore
			}
			
			// build the index
			$indexRdfResource = $semanticsRdfResource;
			$relsExtPredicates = $relsExtRdfResource->getPredicates();
			foreach($relsExtPredicates as $relsExtPredicate)
			{
				// copy only fedora system statements from RELS-EXT, we don't want to copy the DC statements
				if(preg_match('~^info:fedora/fedora-system:def/~', $relsExtPredicate))
				{
					$indexRdfResource->addStatement($relsExtPredicate, $relsExtRdfResource->getObjects($relsExtPredicate));
				}
			}
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/model#createdDate', $createdDate);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/view#lastModifiedDate', $lastModifiedDate);
			$indexRdfResource->addStatement('info:fedora/fedora-system:def/model#state', self::convertStateToUri($objectState));
			
			if($attachmentMimeType !== null)
			{
				$indexRdfResource->addStatement('info:fedora/fedora-system:def/view#mimeType', $attachmentMimeType);
			}
			
			return $indexRdfResource;
		}
		else
		{
			// the object is not active or doesn't exist
			return null;
		}
	}
	
	/**
	 * Returns the PID of this object.
	 * @return string The PID of this object
	 */
	protected function getPid()
	{
		return $this->fedora->convertUriToPid($this->uri);
	}
	
	/**
	 * Get the object state
	 * @return string One of "A", "D", or "I" (Active, Deleted, or Inactive)
	 */
	private function getState()
	{
		return $this->state;
	}

	/**
	 * Convert a Fedora state value to a URI
	 * @param string $state One of "A", "D", or "I" (Active, Deleted, or Inactive)
	 * @return string One of "info:fedora/fedora-system:def/model#Active", "info:fedora/fedora-system:def/model#Deleted", or "info:fedora/fedora-system:def/model#Inactive" (Active, Deleted, or Inactive)
	 */
	private static function convertStateToUri($state)
	{
		if($state == 'A')
		{
			return 'info:fedora/fedora-system:def/model#Active';
		}
		else if($state == 'D')
		{
			return 'info:fedora/fedora-system:def/model#Deleted';
		}
		else if($state == 'I')
		{
			return 'info:fedora/fedora-system:def/model#Inactive';
		}
	}
	
	/**
	 * Get the object state URI
	 * @return string One of "info:fedora/fedora-system:def/model#Active", "info:fedora/fedora-system:def/model#Deleted", or "info:fedora/fedora-system:def/model#Inactive" (Active, Deleted, or Inactive)
	 */
	private function getStateUri()
	{
		return self::convertStateToUri($this->state);
		
	}

	/**
	 * Check if the object is deleted
	 * @return boolean True if deleted
	 */
	public function isDeleted()
	{
		return $this->state === 'D';
	}
	
	/**
	 * Returns the internal URI of this object.
	 * @return string Internal URI of this object
	 */
	public function getUri()
	{
		return $this->uri;
	}

	/**
	 * Returns the external URI of this object.
	 * @return string External URI of this object
	 */
	public function getExternalUri()
	{
		$properties = $this->getProperties();
		$externalUri = $properties->getFirstObject("http://purl.utwente.nl/ns/escape-system.owl#externalUri");
		
		return $externalUri;
	}

	/**
	 * Returns the resource URI of this object.
	 * @return string Resource URI of this object
	 */
	public function getResourceUri()
	{
		$properties = $this->getProperties();
		$resourceUri = $properties->getFirstObject("http://purl.utwente.nl/ns/escape-system.owl#resourceUri");
		
		return $resourceUri;
	}
	
	/**
	 * Get the RDF type uris for this data object.
	 * @return array RDF type URIs
	 */
	public function getRdfTypes()
	{
		return $this->rdfTypes;
	}

	/**
	 * Check if this data object has the RDF type specified.
	 * @param string $rdfType RDF type URI
	 * @return boolean True if this data object has the RDF type specified
	 */
	public function hasRdfType($rdfType)
	{
		return in_array($rdfType, $this->rdfTypes);
	} 
	
	/**
	 * Get the title for this data object using the title URIs specified in the repository schema.
	 * @param boolean $returnAsTriple If true, return the title as a triple array
	 * @return string|array Title as a string or array with the keys 's', 'P' and 'o'
	 */
	public function getTitle($returnAsTriple = false)
	{
		// check for cached title (when overriding this method, this caching method needs to be implemented!)
		if($this->titleTriple === null)
		{
			$properties = $this->getProperties();
			$titlePropertyUriList = $this->repository->repositorySchema->getClassTitlePropertyUriList($this->rdfTypes);
			
			if(count($titlePropertyUriList) > 0)
			{
				foreach($titlePropertyUriList as $titlePropertyUri)
				{
					$titles = $properties->getObjects($titlePropertyUri);
					if(count($titles) > 0)
					{
						// concatenate multiple title values to a single linebreak separated string
						$title = implode("\n", $titles);
						break; 
					}
				}
			} 
			
			if($title !== null)
			{
				$this->titleTriple = array(
								's' => $this->getUri(),
								'P' => $titlePropertyUri,
								'o' => $title,
							);
			}
		}

		if(!$returnAsTriple && $this->titleTriple !== null)
		{
			return $this->titleTriple['o'];
		}
		else
		{
			return $this->titleTriple;
		}
	}

	/**
	 * Sort compare function for sorting by title.
	 * @param escape_repository_DataObject $dataObjectA
	 * @param escape_repository_DataObject $dataObjectB
	 * @return integer 
	 */
	public static function _compareTitle(&$dataObjectA, &$dataObjectB)
	{
	    return strcoll($dataObjectA->getTitle(), $dataObjectB->getTitle());
	}
	
	/**
	 * Returns the creation date.
	 * @return string Creation date
	 */
	public function getCreatedDate()
	{
		return $this->createdDate;
	}
	
	/**
	 * Returns the last modification date.
	 * @return string Last modification date
	 */
	public function getLastModifiedDate()
	{
		return $this->lastModifiedDate;
	}
	
	/**
	 * Returns the attachment MIME type.
	 * @return string Attachment MIME type or NULL if the object doesn't have an attachment
	 */
	public function getAttachmentMimeType()
	{
		return $this->attachmentMimeType;
	}
	
	/**
	 * Returns the attachment file name.
	 * @return string Attachment file name or NULL if the object doesn't have an attachment
	 */
	public function getAttachmentFileName()
	{
		return $this->attachmentFileName;
	}
	
	/**
	 * Add / replace a binary attachement.
	 * @param string $tempFileName location of the binary content
	 * @param string $mimeType MIME type
	 * @param string $fileName file name for the attachment
	 */
	public function saveAttachmentByReference($tempFileName, $mimeType, $fileName)
	{
		// fetch the file content
		$fileContent = file_get_contents($tempFileName);
		
		// store it in fedora
		$this->fedora->modifyDatastream($this->getPid(), "ATTACHMENT", "M", "Binary attachment", $fileContent, $mimeType);
		
		// update this object instance
		$this->attachmentMimeType = $mimeType;
		$this->attachmentFileName = $fileName;
		
		// fetch the properties
		$properties = $this->getProperties();
		
		// replace the external URI with the resource URI
		if($this->getExternalUri() !== null)
		{
			$properties->replaceObject('http://purl.utwente.nl/ns/escape-system.owl#externalUri', $this->getExternalUri(), $this->getResourceUri());
		}

		// store the attachment file name
		$properties->removeStatement('http://purl.utwente.nl/ns/escape-system.owl#attachmentFileName');
		$properties->addStatement('http://purl.utwente.nl/ns/escape-system.owl#attachmentFileName', $fileName);
		
		// store the properties
		$this->setProperties($properties);

		// reindex the object
		$this->updateIndex();
	}
	
	/**
	 * Delete the binary attachment. 
	 */
	public function deleteAttachment()
	{
		// remove the attachment from fedora
		$this->fedora->setDatastreamState($this->getPid(), "ATTACHMENT", "D");
		
		// update this object instance
		$this->attachmentMimeType = null;
		$this->attachmentFileName = null;

		// fetch the properties
		$properties = $this->getProperties();
		
		// replace the external URI with the resource URI
		if($this->getExternalUri() !== null)
		{
			$properties->replaceObject('http://purl.utwente.nl/ns/escape-system.owl#externalUri', $this->getExternalUri(), $this->getResourceUri());
		}

		// remove the attachment file name
		$properties->removeStatement('http://purl.utwente.nl/ns/escape-system.owl#attachmentFileName');

		// store the properties
		$this->setProperties($properties);

		// reindex the object
		$this->updateIndex();
	}
	
	/**
	 * Sends the attachment content to the output. HTTP headers are included.
	 */
	public function outputAttachment()
	{
		if($this->attachmentMimeType !== null)
		{
			// send the headers
			header('Content-Description: File Transfer');
			header('Content-Type: ' . $this->attachmentMimeType);
			
			// set inline / attachment based on mime type
			if(in_array($this->attachmentMimeType, $this->repository->config['inline_mime_types']))
			{
				header('Content-Disposition: inline; filename="' . $this->attachmentFileName . '"');
			}
			else
			{
				header('Content-Disposition: attachment; filename="' . $this->attachmentFileName . '"');
			}
			
			header('Content-Transfer-Encoding: binary');

			//TODO: include file size, this would require Fedora 3.4 ?
			//header('Content-Length: ' . $fileSize);
			
			// send the attachment content to the output
			$this->fedora->outputDatastreamContent($this->getPid(), 'ATTACHMENT');
		}
	}
	
	/**
	 * Copy the attachment content to a file stream.
	 * @param resource $destinationResource Destination file stream
	 */
	public function copyAttachment($destinationResource)
	{
		if($this->attachmentMimeType !== null)
		{
			// save the attachment content
			$this->fedora->copyDatastreamContent($this->getPid(), 'ATTACHMENT', $destinationResource);
		}
	}
	
	/**
	 * Deletes the current object (sets the object state to 'Deleted').
	 * @return string Timestamp
	 */
	public function delete()
	{
		$this->state = 'D';
		
		$timestamp = $this->fedora->modifyObject($this->getPid(), 'D');
		
		$this->updateIndex();
		
		return $timestamp;
	}
	
	/**
	 * Purges the current object.
	 * @return string Timestamp
	 */
	public function purge()
	{
		$this->state = null;
		
		$timestamp = $this->fedora->purgeObject($this->getPid());
		
		$this->updateIndex();
		
		return $timestamp;
	}
	
	/**
	 * Fetches the semantic RDF statements about this resource.
	 * @param boolean $includeInverseStatements If true, inversed RDF statements of objects linking to this object are included in the result
	 * @return simplerdf_RdfResource RDF statements about this resource as an RdfResource object
	 */
	public function getProperties($includeInverseStatements = false)
	{
		if($includeInverseStatements)
		{
			// fetch the statements
			$rdfResource = $this->properties;
			
			// fetch incoming statements
			$inverseRdfDocument = $this->getIncomingStatements();

			$inverseRdfResources = $inverseRdfDocument->getResources();
			foreach($inverseRdfResources as $inverseRdfResource)
			{
				$subject = $inverseRdfResource->getSubject();
				$predicates = $inverseRdfResource->getPredicates();
				foreach($predicates as $predicate)
				{
					$inversePredicate = $this->repository->repositorySchema->getInversePropertyUri($predicate);
					if($inversePredicate !== null)
					{
						$rdfResource->addStatement($inversePredicate, $subject);
					}
				}
			}
			
			return $rdfResource;
		}
		else
		{
			return $this->properties;
		}
	}
	
	/**
	 * Get incoming statements (not inversed).
	 * @return simplerdf_RdfDocument Incoming statements 
	 */
	public function getIncomingStatements()
	{
		$rdfDocument = $this->tripleStore->fetchStatements(null, null, $this->getUri(), false, null);
		
		return $rdfDocument;
	}
	
	/**
	 * Updates the semantic RDF statements with the RDF statements provided.
	 * @param simplerdf_RdfResource $properties RDF statements as an simplerdf_RdfResource object
	 * @return string Timestamp
	 */
	public function setProperties(&$properties)
	{
		$this->properties =& $properties;
		$this->rdfTypes = $properties->getRdfTypes();
		$this->titleTriple = null;
		$timestamp = $this->fedora->modifyDatastreamByValueString($this->getPid(), "SEMANTICS", $properties->getRdfXml());
		
		$this->lastModifiedDate = $timestamp;
		
		$this->updateIndex();
		
		return $timestamp;
	}
}